大作业开发日志
1.1 词法阶段
ToyPL 风格词法分析实现规划
摘要
按 README.md 里整理出的 ToyPL 词法风格,先把作业的词法阶段单独做完,不进入语法和语义。实现目标是:把源代码稳定切成 Token,保留位置信息,支持题目要求的 int / float / string 常量、int / float 变量、6 种比较运算、if-else、print,并为可 选的 while / for 留好入口。
关键改动
- 更新 src/main/java/com/weiqiang/caculator/lex/TokenType.java,把当前不完整的枚举扩成完整词法集合。
- 新增 src/main/java/com/weiqiang/caculator/lex/Token.java,保存 type / value / row / col,风格对齐 ToyPL。
- 新增 src/main/java/com/weiqiang/caculator/lex/Lexer.java,采用 单指针 + 当前行 + 当前列 + 前瞻字符 的扫描方式。
- 新增 src/main/java/com/weiqiang/caculator/lex/MyException.java,统一抛出词法错误,错误信息带行列位置。
- 词法阶段只负责“识别并产出 Token”,不做语法树构建,也不做类型检查。
Token 设计
- 最小必需 Token:INT、FLOAT、STRING、ID、KW、IF、ELSE、PRINT、PLUS、MINUS、MUL、DIV、ASSIGN、COMP、LPAREN、RPAREN、 LBRACE、RBRACE、COMMA、SEMICOLON、EOF。
- 其中 KW 用来表示类型关键字,value 区分 int 和 float,这样最接近 ToyPL 的编码方式。
- COMP 统一表示 6 种比较运算,value 保存具体符号:> >= < <= == !=。
- WHILE / FOR 作为可选扩展 Token 预留;如果你后面确认只做最小作业,也可以在 lexer 中先识别但不进入后续阶段。
- MOD、AND、OR、NOT 这类 ToyPL 扩展项不纳入当前作业最小词法集,避免把词法范围做大。
扫描规则
- 空白符、制表符、换行符直接跳过,换行只更新行号和列号。
- 支持 // 单行注释,注释内容直到行尾都忽略。
- 数字扫描:连续读取数字;遇到一个 . 就转为浮点扫描;若出现第二个 .,直接报错。
- 字符串扫描:遇到 " 进入字符串状态,直到再次遇到 " 结束;如果到行尾或文件结束仍未闭合,报错。
- 标识符扫描:以字母或下划线开头,后续可跟字母、数字、下划线;扫描完后查关键字表。
- 关键字识别顺序:先识别完整单词,再判断是否为 int / float / if / else / print / while / for。
- 运算符识别顺序:先看双字符运算符,再看单字符运算符;例如 ==、<=、>=、!= 必须优先于 =、<、>、!。
- 扫描结束后追加一个 EOF,和 ToyPL 一致。
状态处理
- analyzeTokensFromCode(String code):按行切分源码,逐行调用扫描逻辑。
- analyzeTokensFromLine():在当前行上循环推进,遇到一个 Token 就立刻入表。
- watchNextChar():只负责“看当前字符”,不前移指针,保持 ToyPL 式的前瞻结构。
- extractNumberToken():专门抽取数字和小数。
- extractStringToken():专门抽取字符串常量。
- extractIdentifierOrKeyword():专门抽取标识符和关键字。
- 扫描器内部建议用 StringBuilder 收集字面量内容,不用正则硬切,这样更贴近 ToyPL 的逐字符写法。
- 所有状态转换都围绕当前字符种类做分派,保持实现简单、可读、易调试。
错误处理
- 统一抛出 MyException,消息格式建议包含:
- 错误代码
- 原始行内容
- 行号
- 列号
- 错误原因
- 至少处理以下错误:
- 非法字符
- 数字格式错误
- 重复小数点
- 字符串未闭合
- 非法运算符组合
- 建议错误风格直接仿 ToyPL:一旦发现错误立即停止,不做容错继续扫描。
- 词法阶段不处理括号是否配对、不处理语句是否完整,这些留给语法阶段。
测试
- 基本识别测试:
- int a = 12;
- float b = 3.14;
- print("hello");
- 比较运算测试:
- a > b
- a >= b
- a < b
- a <= b
- a == b
- a != b
- 控制流测试:
- if (a < b) print(a); else print(b);
- while (a < 10) print(a);
- for (...) 若你选择保留扩展
- 注释和空白测试:
- 含空格、换行、// 注释混排的输入
- 错误测试:
- 12.3.4
- "abc
- @
- a = = 3
- 验收标准:
- 合法输入能稳定产出正确 Token 序列
- 每个 Token 都带准确行列信息
- 非法输入能准确报错并停止
默认假设
- 当前作业词法只做“识别”,不做语法和语义。
- 类型关键字只需要 int 和 float,因为黑板题明确变量只支持这两类。
- string 只作为字符串常量处理,不作为变量类型处理。
- 为了贴近 ToyPL,比较运算统一用 COMP,具体符号放在 Token.value 中。
- while / for 属于可选项,所以 lexer 先预留 Token,但不会影响最小作业实现。
已按你的要求处理:
- src/main/java/com/weiqiang/caculator/lex/Token.java
- src/main/java/com/weiqiang/caculator/lex/Lexer.java
- src/main/java/com/weiqiang/caculator/lex/MyException.java
实现内容包括:
- 识别 int / float / string、ID、if / else / while / for / print
- 识别 = == != < <= > >=
- 识别 + - * / ( ) { } [ ] , ;
- 支持 // 注释
- 保留行号、列号和 EOF
- 词法错误统一抛 MyException
我还顺手把 pom.xml 配成了 Java 8 + UTF-8,并把 D:/project/javacode/caculator.gitignore 加上了 META-INF/ 和 tmp_jar/。
验证结果:mvn -q -DskipTests package 已通过。
1.2 词法阶段
语法树(初版)
Project AST
program -> block
block -> exprList
exprList -> stmt
stmt -> declare
declare -> kw int id [;]
stmt -> declare
declare -> kw float id [= expr]
expr -> arith
arith -> term
term -> factor FLOAT
stmt -> expr
expr -> IF (arith) block [ELSE block]
arith -> term
term -> factor ID
block -> exprList
stmt -> print
print -> CALL print(arith)
arith -> term
term -> factor STRING
stmt -> print
print -> CALL print(arith)
arith -> term
term -> factor STRING
stmt -> print
print -> CALL print(arith)
arith -> term
term -> factor ID
term -> factor BINARY(+)
factor -> ID
factor -> ID我做了两件事:
变量声明和赋值
print(...)
if-else
while
for
+ - * / %
> >= < <= == !=
括号表达式
数字 / 字符串 / 标识符
我也把 SyntaxNode.toTreeString() 做好了,后面你可以直接把 AST 打出来看树结构。
- mvn -q -DskipTests package 已通过
如果你要,我下一步可以继续把这个语法树接到一个“示例入口”里,给你写一个最小 main 来打印整棵树,方便你直接看解析结果。